﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace Mclib
{
	/// <summary>
	/// Represents an axis in the Pb and all associated plots inside the axis.
	/// </summary>
	public class Axis
	{
		/// <summary>
		/// Determines whether the axis border (line) is drawn on each side of the client rect.
		/// </summary>
		public bool[] DrawLineLRTB = new bool[] { true,false,false,true };
		/// <summary>
		/// Determines whether the text for each tick is drawn on each side of the client rect.
		/// Note that text is only drawn when corresponding elements of DrawLineLRTB are also true.
		/// </summary>
		public bool[] DrawTickLabelsLRTB = new bool[] { true,true,true,true };
		/// <summary>
		/// The rectangle of the client area LRTB[0,1,0,1] relative to the container rectangle.
		/// This positions the axis's internal (client) area (where the data is drawn) with respect to the rectangle contained within.
		/// </summary>
		public RectangleF ClientRect = new RectangleF(0.08f,0.04f,0.88f,0.88f); // (0.1f, 0.02f, 0.88f, 0.88f) (0.1f,0.1f,0.78f,0.78f)
		/// <summary>
		/// The rectangle corresponding to AxisDrawRect but in Data-coordinates.
		/// In this case, the "Bottom" is really the Top and the "Top" is really the bottom.
		/// </summary>
		public RectangleF DataRect = new RectangleF(0.0f,0.0f,1.0f,1.0f);
		/// <summary>
		/// True if a grid is drawn, false otherwise.
		/// </summary>
		public bool DrawGrid = true;
		/// <summary>
		/// If label strings should be automatically generated for ticks, set to true.
		/// </summary>
		public bool AutoLabelTicks = true;
		/// <summary>
		/// The tick-marks of the horizontal data axis.  AutoLabels are updated automatically when this float[] is changed,
		/// but if any member of the array is accessed and changed, the AutoLabels are not updated.
		/// </summary>
		public float[] DataTickX
		{
			get
			{
				return dataTickX;
			}
			set
			{
				dataTickX = value;
				if (DataTickX_AutoLabels==null || DataTickX_AutoLabels.Length!=dataTickX.Length)
					DataTickX_AutoLabels = new string[dataTickX.Length];
				for (int i=0; i<dataTickX.Length; i++)
					DataTickX_AutoLabels[i] = nice.ShortString((double)dataTickX[i]);
			}
		}
		private float[] dataTickX = new float[] { 0.0f,0.5f,1.0f };
		public string[] DataTickX_AutoLabels = new string[3];
		/// <summary>
		/// The tick-marks of the vertical data axis.  AutoLabels are updated automatically when this float[] is changed,
		/// but if any member of the array is accessed and changed, the AutoLabels are not updated.
		/// </summary>
		public float[] DataTickY
		{
			get
			{
				return dataTickY;
			}
			set
			{
				dataTickY = value;
				if (DataTickY_AutoLabels==null || DataTickY_AutoLabels.Length!=dataTickY.Length)
					DataTickY_AutoLabels = new string[dataTickY.Length];
				for (int i=0; i<dataTickY.Length; i++)
					DataTickY_AutoLabels[i] = nice.ShortString((double)dataTickY[i]);
			}
		}
		private float[] dataTickY = new float[] { 0.0f,0.5f,1.0f };
		public string[] DataTickY_AutoLabels = new string[3];
		/// <summary>
		/// The scale with which to draw the axis.  Automatically scales the font and the pen.
		/// </summary>
		public float DrawScale
		{
			get { return drawScale; }
			set
			{
				if (value!=drawScale)
				{
					drawScale = value;
					scaledAxisFont = new Font(axisFont.FontFamily,axisFont.Size*drawScale);
					scaledAxisPen = new Pen(axisPen.Color,axisPen.Width*drawScale);
					scaledGridPen = new Pen(gridPen.Color,gridPen.Width*drawScale);
				}
			}
		}
		private float drawScale = 1.0f;
		/// <summary>
		/// The pen used for drawing the axis, width is automatically scaled by DrawScale.
		/// </summary>
		public Pen AxisPen
		{
			get { return axisPen; }
			set
			{
				axisPen = value;
				scaledAxisPen = new Pen(axisPen.Color,axisPen.Width*drawScale);
			}
		}
		private Pen axisPen = new Pen(Color.Black,1.0f);
		private Pen scaledAxisPen = new Pen(Color.Black,1.0f);
		/// <summary>
		/// The pen used for drawing the axis, width is automatically scaled by DrawScale.
		/// </summary>
		public Pen GridPen
		{
			get { return gridPen; }
			set
			{
				gridPen = value;
				scaledGridPen = new Pen(gridPen.Color,gridPen.Width*drawScale);
			}
		}
		private Pen gridPen = new Pen(Color.LightGray,1.0f);
		private Pen scaledGridPen = new Pen(Color.LightGray,1.0f);
		/// <summary>
		/// The font used to draw the axis, size is automatically scaled by DrawScale.
		/// </summary>
		public Font AxisFont
		{
			get { return axisFont; }
			set
			{
				axisFont = value;
				scaledAxisFont = new Font(axisFont.FontFamily,axisFont.Size*drawScale);
			}
		}
		private Font axisFont = new Font(new FontFamily("Arial Narrow"),14.0f);
		private Font scaledAxisFont = new Font(new FontFamily("Arial Narrow"),14.0f);
		/// <summary>
		/// The string format for axis tick mark labels.
		/// </summary>
		public StringFormat[] TickStringFormats = new StringFormat[4];
		/// <summary>
		/// The pen used for drawing the axis, width is automatically scaled by DrawScale.
		/// </summary>
		public float TickLength=0.025f;
		/// <summary>
		/// The plots in this axis.
		/// </summary>
		public List<object> Children = new List<object>();
		private PointF[,] xTicks,yTicks;
		public Axis()
		{
			//--------------------------------------------------
			// TickStringFormat
			//--------------------------------------------------
			//	Left
			TickStringFormats[0] = new StringFormat();
			TickStringFormats[0].Alignment = StringAlignment.Far;  // Horizontal Align Bottom
			TickStringFormats[0].LineAlignment = StringAlignment.Center; // Vertical Align Center

			//	Right
			TickStringFormats[1] = new StringFormat();
			TickStringFormats[1].Alignment = StringAlignment.Near;  // Horizontal Align Top
			TickStringFormats[1].LineAlignment = StringAlignment.Center; // Vertical Align Center

			//	Top
			TickStringFormats[2] = new StringFormat();
			TickStringFormats[2].Alignment = StringAlignment.Center;  // Horizontal Align Center
			TickStringFormats[2].LineAlignment = StringAlignment.Far; // Vertical Align Right

			//	Bottom
			TickStringFormats[3] = new StringFormat();
			TickStringFormats[3].Alignment = StringAlignment.Center;  // Horizontal Align Center
			TickStringFormats[3].LineAlignment = StringAlignment.Near; // Vertical Align Left
			//--------------------------------------------------

			//--------------------------------------------------
			// AutoLabels for ticks
			//--------------------------------------------------
			this.DataTickX = dataTickX;
			this.DataTickY = dataTickY;
			//--------------------------------------------------
		}
		/// <summary>
		/// Paints the instance onto the control with PaintEventArgs e.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e">The paint event args for the entire plotting surface.</param>
		public void OnPaint(object sender,PaintEventArgs e)
		{
			e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
			//	pi.Rect is the rectangle (in Pb client coordinates) where I paint the axis.
			PaintingInfo pi = (PaintingInfo)sender;
			//	piClient.Rect is the rectangle inside which the axes are painted.
			PaintingInfo piClient = pi.CreateChild(ClientRect);
			//	(int) rounding ensures sharp lines are drawn
			piClient.Rect = new RectangleF((int)piClient.Rect.X,(int)piClient.Rect.Y,(int)piClient.Rect.Width,(int)piClient.Rect.Height);
			//	Draw the children
			int iChild=0;
			//	Child type
			Type chType;

			//	Pre-computed sclars
			float hScale = piClient.Rect.Height/DataRect.Height;
			float wScale = piClient.Rect.Width/DataRect.Width;
			//	Tick indexers
			int iTick;
			//	---------------------------------------------------------------
			//	Calculate tick points and draw the grid
			//	---------------------------------------------------------------
			if (xTicks==null || xTicks.GetLength(0)<this.dataTickX.Length)
				xTicks = new PointF[this.dataTickX.Length,2];
			for (iTick=0; iTick<this.dataTickX.Length; iTick++)
			{
				//	Top
				xTicks[iTick,0] = new PointF((int)(0.001f+(dataTickX[iTick]-DataRect.Left)*wScale+piClient.Rect.Left),piClient.Rect.Top);
				//	Bottom
				xTicks[iTick,1] = new PointF(xTicks[iTick,0].X,piClient.Rect.Bottom);
				//	Draw
				if (this.DrawGrid)
					e.Graphics.DrawLine(this.gridPen,xTicks[iTick,0],xTicks[iTick,1]);
			}

			if (yTicks==null || yTicks.GetLength(0)<this.dataTickY.Length)
				yTicks = new PointF[this.dataTickY.Length,2];
			for (iTick=0; iTick<this.dataTickY.Length; iTick++)
			{
				//	Left
				yTicks[iTick,0] = new PointF(piClient.Rect.Left,(int)(0.001f+(this.DataRect.Bottom-dataTickY[iTick])*hScale+piClient.Rect.Top)); // y is inverted from data to screen coordinate
				//	Right
				yTicks[iTick,1] = new PointF(piClient.Rect.Right,yTicks[iTick,0].Y);
				//	Draw
				if (this.DrawGrid)
					e.Graphics.DrawLine(this.gridPen,yTicks[iTick,0],yTicks[iTick,1]);
			}
			//	---------------------------------------------------------------

			//	---------------------------------------------------------------
			//	Paint all children
			//	---------------------------------------------------------------
			for (iChild=0; iChild<Children.Count; iChild++)
			{
				chType = Children[iChild].GetType();
				if (chType==typeof(DataSeries))
					paintDataSeries((DataSeries)Children[iChild],drawScale,piClient,e);
				if (chType==typeof(DataSurface))
					paintDataSurface((DataSurface)Children[iChild],drawScale,piClient,e);
				if (chType==typeof(DataSurfaces))
					paintDataSurfaces((DataSurfaces)Children[iChild],drawScale,piClient,e);
				if(chType==typeof(IsoContours))
					paintIsoContours((IsoContours)Children[iChild],drawScale,piClient,e);
			}
			//	Paint DataSeries children again using a thinner draw scale.
			for (iChild=Children.Count-1; iChild>=0; iChild--)
			{
				chType = Children[iChild].GetType();
				if (chType==typeof(DataSeries))
					paintDataSeries((DataSeries)Children[iChild],0.5f*drawScale,piClient,e);
			}
			//	---------------------------------------------------------------

			//	Axis points
			PointF axMin,axMax,axTick1,axTick2;
			float halfTick = 0.5f*TickLength*piClient.Rect.Width;
			//	---------------------------------------------------------------
			//	Draw the axes
			//	---------------------------------------------------------------
			//	Left
			if (DrawLineLRTB[0])
			{
				axMin = new PointF(piClient.Rect.Left,piClient.Rect.Bottom);
				axMax = new PointF(piClient.Rect.Left,piClient.Rect.Top);
				e.Graphics.DrawLine(this.scaledAxisPen,axMin,axMax);
				for (iTick=0; iTick<this.dataTickY.Length; iTick++)
				{
					//	(int) rounding ensures sharp lines are drawn
					//	y-axis is reversed and DataRect.Bottom is actually the top.
					axTick1 = new PointF(yTicks[iTick,0].X-halfTick,yTicks[iTick,0].Y);
					axTick2 = new PointF(yTicks[iTick,0].X+halfTick,yTicks[iTick,0].Y);
					e.Graphics.DrawLine(this.scaledAxisPen,axTick1,axTick2);
					//	Draw stuff to the left of the tick
					if (this.AutoLabelTicks)
						e.Graphics.DrawString(
							this.DataTickY_AutoLabels[iTick],
							scaledAxisFont,
							axisPen.Brush,
							new PointF(yTicks[iTick,0].X-1.025f*halfTick,yTicks[iTick,0].Y),
							TickStringFormats[0]);
				}
			}
			//	Right
			if (DrawLineLRTB[1])
			{
				axMin = new PointF(piClient.Rect.Right,piClient.Rect.Bottom);
				axMax = new PointF(piClient.Rect.Right,piClient.Rect.Top);
				e.Graphics.DrawLine(this.scaledAxisPen,axMin,axMax);
				for (iTick=0; iTick<this.dataTickY.Length; iTick++)
				{
					//	(int) rounding ensures sharp lines are drawn
					//	y-axis is reversed and DataRect.Bottom is actually the top.
					axTick1 = new PointF(yTicks[iTick,1].X-halfTick,yTicks[iTick,1].Y);
					axTick2 = new PointF(yTicks[iTick,1].X+halfTick,yTicks[iTick,1].Y);
					e.Graphics.DrawLine(this.scaledAxisPen,axTick1,axTick2);
					//	Draw stuff to the right of the tick
					if (this.AutoLabelTicks)
						e.Graphics.DrawString(
							this.DataTickY_AutoLabels[iTick],
							scaledAxisFont,
							axisPen.Brush,
							new PointF(yTicks[iTick,1].X+1.025f*halfTick,yTicks[iTick,1].Y),
							TickStringFormats[1]);
				}
			}
			//	Top
			halfTick = 0.5f*TickLength*piClient.Rect.Height;
			if (DrawLineLRTB[2])
			{
				axMin = new PointF(piClient.Rect.Left,piClient.Rect.Top);
				axMax = new PointF(piClient.Rect.Right,piClient.Rect.Top);
				e.Graphics.DrawLine(this.scaledAxisPen,axMin,axMax);
				for (iTick=0; iTick<this.dataTickX.Length; iTick++)
				{
					//	(int) rounding ensures sharp lines are drawn
					//	y-axis is reversed and DataRect.Bottom is actually the top.
					axTick1 = new PointF(xTicks[iTick,0].X,xTicks[iTick,0].Y-halfTick);
					axTick2 = new PointF(xTicks[iTick,0].X,xTicks[iTick,0].Y+halfTick);
					e.Graphics.DrawLine(this.scaledAxisPen,axTick1,axTick2);
					//	Draw stuff above the tick
					if (this.AutoLabelTicks)
						e.Graphics.DrawString(
							this.DataTickX_AutoLabels[iTick],
							scaledAxisFont,
							axisPen.Brush,
							new PointF(xTicks[iTick,0].X,xTicks[iTick,0].Y-1.025f*halfTick),
							TickStringFormats[2]);
				}
			}
			//	Bottom
			if (DrawLineLRTB[3])
			{
				axMin = new PointF(piClient.Rect.Left,piClient.Rect.Bottom);
				axMax = new PointF(piClient.Rect.Right,piClient.Rect.Bottom);
				e.Graphics.DrawLine(this.scaledAxisPen,axMin,axMax);
				for (iTick=0; iTick<this.dataTickX.Length; iTick++)
				{
					//	(int) rounding ensures sharp lines are drawn
					//	y-axis is reversed and DataRect.Bottom is actually the top.
					axTick1 = new PointF(xTicks[iTick,1].X,xTicks[iTick,1].Y-halfTick);
					axTick2 = new PointF(xTicks[iTick,1].X,xTicks[iTick,1].Y+halfTick);
					e.Graphics.DrawLine(this.scaledAxisPen,axTick1,axTick2);
					//	Draw stuff below the tick
					if (this.AutoLabelTicks)
						e.Graphics.DrawString(
							this.DataTickX_AutoLabels[iTick],
							scaledAxisFont,
							axisPen.Brush,
							new PointF(xTicks[iTick,1].X,xTicks[iTick,1].Y+1.025f*halfTick),
							TickStringFormats[3]);
				}
			}
			//	---------------------------------------------------------------
		}
		/// <summary>
		/// Paints a data series on the current axis.
		/// </summary>
		/// <param name="ds">The data series to be painted.</param>
		/// <param name="scale">The scale of any lines or text on the plot.</param>
		/// <param name="pi">pi.Rect is the rectangle (client area of the axis) in which the data series should be plotted.</param>
		/// <param name="e">The painting context and related information.</param>
		private void paintDataSeries(DataSeries ds,float scale,PaintingInfo pi,PaintEventArgs e)
		{
			//	Scalar for width
			float sWidth=pi.Rect.Width/DataRect.Width;
			//	Scalar for height
			float sHeight=pi.Rect.Height/DataRect.Height;
			PointF[] pf = new PointF[ds.Series.Length];
			for (int i=0; i<pf.Length; i++)
			{
				pf[i] = new PointF(
					pi.Rect.Left+sWidth*(ds.Series[i].X-DataRect.Left),
					pi.Rect.Top+sHeight*(DataRect.Bottom-ds.Series[i].Y)); // y-axis is reversed and DataRect.Bottom is actually the top.
			}
			Pen pen = new Pen(ds.Pen.Color,ds.Pen.Width*scale);
			if (pen.Width>=1.0f)
				e.Graphics.DrawLines(pen,pf);
		}
		/// <summary>
		/// Paints a data series on the current axis.
		/// </summary>
		/// <param name="ds">The data series to be painted.</param>
		/// <param name="scale">The scale of any lines or text on the plot.</param>
		/// <param name="pi">pi.Rect is the rectangle (client area of the axis) in which the data series should be plotted.</param>
		/// <param name="e">The painting context and related information.</param>
		private void paintIsoContours(IsoContours iso,float scale,PaintingInfo pi,PaintEventArgs e)
		{
			Pen pen = new Pen(iso.Pen.Brush,iso.Pen.Width*scale);
			if(pen.Width>=0.5f)
			{
				PointF[] con;
				int iCon;
				for(iCon=0; iCon<iso.Contours.Count; iCon++)
				{
					if(iso.Contours[iCon].Vertices.Length>1)
					{
						con = iso.Contours[iCon].Paintable(pi);
						e.Graphics.DrawLines(pen,con);
						if(iso.Contours[iCon].IsClosed)
							e.Graphics.DrawLine(pen,con[0],con[con.Length-1]);
					}
					else
						break; // The contours are sorted by length, so once you hit a length==1, break.
				}
				/*
				PointF[] vec;
				int i;
				for(i=0; i<iso.Vertices.Count; i++)
				{
					vec = iso.Vertices[i].Paintable(pi);
					e.Graphics.DrawLines(pen,vec);
					e.Graphics.DrawEllipse(pen,vec[0].X-2.0f,vec[0].Y-2.0f,4.0f,4.0f);
				}
				*/
			}

		}
		/// <summary>
		/// Paints a data surface on the current axis.
		/// </summary>
		/// <param name="ds">The data surface to be painted.</param>
		/// <param name="scale">The scale of any lines or text on the plot.</param>
		/// <param name="pi">pi.Rect is the rectangle (client area of the axis) in which the data series should be plotted.</param>
		/// <param name="e">The painting context and related information.</param>
		private void paintDataSurface(DataSurface ds, float scale, PaintingInfo pi, PaintEventArgs e)
		{
			Bitmap bmp = ds.Bmp;
			RectangleF srcRect = new RectangleF(0.0f,0.0f,bmp.Width-1,bmp.Height-1);
			e.Graphics.DrawImage(bmp,pi.Rect,srcRect,GraphicsUnit.Pixel);
		}
		/// <summary>
		/// Paints data surfaces on the current axis.
		/// </summary>
		/// <param name="ds">The data surfaces to be painted.</param>
		/// <param name="scale">The scale of any lines or text on the plot.</param>
		/// <param name="pi">pi.Rect is the rectangle (client area of the axis) in which the data series should be plotted.</param>
		/// <param name="e">The painting context and related information.</param>
		private void paintDataSurfaces(DataSurfaces ds, float scale, PaintingInfo pi, PaintEventArgs e)
		{
			Bitmap bmp = ds.Bmp;
			RectangleF srcRect = new RectangleF(0.0f,0.0f,bmp.Width-1,bmp.Height-1);
			e.Graphics.DrawImage(bmp,pi.Rect,srcRect,GraphicsUnit.Pixel);
		}
		/// <summary>
		/// Something for making nice numbers.
		/// </summary>
		protected NiceNumberMaker nice = new NiceNumberMaker();
	}
}